/*
 * Galaxium Messenger
 * 
 * Copyright (C) 2007 Ben Motmans <ben.motmans@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.Text;
using System.Collections.Generic;

using Galaxium.Gui;
using Galaxium.Core;
using Galaxium.Protocol;

using Anculus.Core;

namespace Galaxium.Protocol.Irc
{
	public sealed class IrcConversation : AbstractConversation
	{
		public event EventHandler<ChannelContactEventArgs> ChannelContactInitialJoin;
		public event EventHandler<ChannelContactEventArgs> ChannelContactJoin;
		public event EventHandler<ChannelContactEventArgs> ChannelContactPart;
		public event EventHandler<ChannelContactMessageEventArgs> ChannelContactQuit;
		public event EventHandler<ChannelContactActionEventArgs> ChannelContactKill;
		public event EventHandler<ChannelContactActionEventArgs> ChannelContactKick;
		public event EventHandler<ChannelContactModeEventArgs> ChannelContactModeChanged;
		
		public event EventHandler<ObjectEventArgs> ChannelBanned;
		public event EventHandler<ObjectEventArgs> ChannelInviteOnly;
		public event EventHandler<ChannelModeEventArgs> ChannelModeChanged;
		public event EventHandler<MessageEventArgs> NoticeReceived;
		public event EventHandler<ObjectEventArgs> DCCStateChanged;
		public event EventHandler<EntityChangeEventArgs<string>> ChannelTopicChanged;
		
		private IrcContact me;
		
		private bool isDCCConversation;
		private bool isPrivateConversation;
		private bool isInitialized;
		private bool isJoined;
		private Dictionary<string, string> _contactUidLookup;
		private DccChatConnection _dccChat;
		
		public DccChatConnection ChatConnection
		{
			get { return _dccChat; }
			set
			{
				if (value == null)
				{
					_dccChat.AfterConnect += HandleChatEstablished;
					_dccChat.Closed += HandleChatClosed;
					_dccChat.MessageReceived -= HandleMessageReceived;
					_dccChat.ActionReceived -= HandleActionReceived;
					_dccChat = null;
					
					OnDCCStateChanged (new ObjectEventArgs (null));
				}
				else if (value != _dccChat)
				{
					if (_dccChat != null)
					{
						_dccChat.AfterConnect -= HandleChatEstablished;
						_dccChat.Closed -= HandleChatClosed;
						_dccChat.MessageReceived -= HandleMessageReceived;
						_dccChat.ActionReceived -= HandleActionReceived;
					}
					
					_dccChat = value;
					_dccChat.AfterConnect += HandleChatEstablished;
					_dccChat.Closed += HandleChatClosed;
					_dccChat.MessageReceived += HandleMessageReceived;
					_dccChat.ActionReceived += HandleActionReceived;
					
					OnDCCStateChanged (new ObjectEventArgs (_dccChat));
				}
			}
		}
		
		public void OnDCCStateChanged (ObjectEventArgs args)
		{
			ThreadUtility.Dispatch (() => {
			if (DCCStateChanged != null)
				DCCStateChanged (this, new ObjectEventArgs (null));
			});
		}
		
		private void HandleChatEstablished(object sender, ConnectionEventArgs e)
		{
			OnDCCStateChanged(new ObjectEventArgs (_dccChat));
		}
		
		private void HandleChatClosed(object sender, ConnectionEventArgs e)
		{
			OnDCCStateChanged(new ObjectEventArgs (null));
		}
		
		private void HandleActionReceived(object sender, DccChatEventArgs e)
		{
			
		}

		private void HandleMessageReceived (object sender, DccChatEventArgs e)
		{
			EmitMessageReceived (PrimaryContact as IrcContact, e.Message);
		}
		
		public IrcConversation (IrcSession session, IrcChannel channel)
			: base (channel, session)
		{
			Initialize ();
			isPrivateConversation = false;
			isJoined = false;
			channel.Presence = IrcPresence.Offline;
			channel.TopicChanged += HandleTopicChanged;
			channel.ChannelModeChanged += HandleChannelModeChanged;
			
			_contacts = channel.ContactCollection;
		}
		
		public IrcConversation (IrcSession session, IrcContact contact)
			: base (contact, session)
		{
			isPrivateConversation = true;
			isJoined = false;
			_contacts = new ContactCollection ();
		}
		
		private void Initialize ()
		{
			_contactUidLookup = new Dictionary<string,string> ();

			Session.Account.DisplayNameChange += delegate (object sender, EntityChangeEventArgs<string> args) {
				if (me != null)
					me.DisplayName = args.New;
			};
		}
		
		public void ConnectDCC ()
		{
			IrcSession session = Session as IrcSession;
			
			string ip = String.Empty;
			int port = NetworkUtility.GetUnusedPort ();
			
			if (NetworkUtility.UseNAT)
			{
				byte [] array = NetworkUtility.IPAddress.GetAddressBytes();
				Array.Reverse (array);
				ip = BitConverter.ToInt32 (array, 0).ToString ();
			}
			else
			{
				byte [] array = NetworkUtility.LocalIPAddress.GetAddressBytes();
				Array.Reverse (array);
				ip = BitConverter.ToInt32 (array, 0).ToString ();
			}
			
			StringBuilder dcc = new StringBuilder ();
			dcc.Append (IrcConstants.Markers.Ctcp);
			dcc.Append (IrcConstants.Commands.Dcc);
			dcc.Append (" CHAT ");
			
			dcc.Append ("chat");
			dcc.Append (' ');
			dcc.Append (ip);
			dcc.Append (' ');
			dcc.Append (port);
			
			dcc.Append (IrcConstants.Markers.Ctcp);
			
			session.SendPrivateMessage (PrimaryContact.DisplayName, dcc.ToString ());
			
			// If this will be a private chat, why dont we try DCC chat?
			DccConnectionInfo connectInfo = new DccConnectionInfo (false, ip, port);
			ChatConnection = new DccChatConnection (session, this, connectInfo);
			_dccChat.Connect();
		}
		
		public IrcContact GetContact (string displayName)
		{
			if (displayName == null)
				throw new ArgumentNullException ("displayName");
			
			string uid;
			if (_contactUidLookup.TryGetValue (displayName, out uid))
				return _contacts.GetContact (uid) as IrcContact;
			return null;
		}

		public IrcContact Me
		{
			get { return me; }
		}
		
		public override bool IsPrivateConversation
		{
			get { return isPrivateConversation; }
		}
		
		public override bool IsChannelConversation
		{
			get { return !isPrivateConversation; }
		}
		
		public bool IsInitialized
		{
			get { return isInitialized; }
			internal set { isInitialized = value; }
		}
		
		public bool IsJoined
		{
			get { return isJoined; }
			internal set { isJoined = value; _primaryContact.Presence = value ? IrcPresence.Online : IrcPresence.Offline; }
		}
		
		public bool IsMe (string name)
		{
			return me.DisplayName == name;
		}
		
		public override void InviteContact (IContact contact)
		{
			
		}

		/*public override IFileTransfer SendFile (IContact contact, string fileName)
		{
			if (isPrivateConversation) {
				//TODO: implement
				return null;
			} else {
				throw new NotSupportedException ();
			}
		}*/

		public override void Close ()
		{
			if (isJoined && !isPrivateConversation && !(_primaryContact as IrcChannel).Persistent)
			{
				IrcSession session = _session as IrcSession;
				
				if (!session.Quitting)
					session.PartChannels (PrimaryContact.UniqueIdentifier);
			}
			
			IsInitialized = false;
			
			OnClosed (new ConversationEventArgs (this));
		}
		
		public void SendMessage (string message)
		{
			if (String.IsNullOrEmpty (message))
				throw new ArgumentException ("message");
			
			IrcSession session = _session as IrcSession;
			string target = _primaryContact.DisplayName;
			if (isPrivateConversation)
			{
				// If we have a private chat, we need to know if we are going to send
				// this using the DCC connection, or the regular server message.
				if (_dccChat != null && _dccChat.IsConnected)
					_dccChat.SendPrivateMessage (message);
				else
					session.SendPrivateMessage (target, message);
			}
			else
				session.SendMessage (target, message);
			
			IMessage msg = new Message (MessageFlag.Message, Session.Account, null);
			msg.SetMarkup (message, null);
			
			LogMessage (msg, true);
		}
		
		public override void AddContact (IContact contact)
		{
			if (contact.DisplayName == Session.Account.DisplayName)
				me = contact as IrcContact;
			
			if (_contactUidLookup.ContainsKey (contact.DisplayName))
				_contactUidLookup[contact.DisplayName] = contact.UniqueIdentifier;
			else
				_contactUidLookup.Add (contact.DisplayName, contact.UniqueIdentifier);
			
			(contact as IrcContact).Conversation = this;
			
			contact.DisplayNameChange += HandleContactNameChanged;
			
			base.AddContact (contact);
		}

		public override void RemoveContact (IContact contact)
		{
			_contactUidLookup.Remove (contact.DisplayName);
			
			(contact as IrcContact).Conversation = null;
			
			contact.DisplayNameChange -= HandleContactNameChanged;
			
			base.RemoveContact (contact);
		}
		
		public void HandleContactNameChanged (object sender, EntityChangeEventArgs<string> args)
		{
			_contactUidLookup.Remove (args.Old);
			
			LogEvent (String.Format ("{0} is now known as {1}", args.Old, args.New));
			
			if (_contactUidLookup.ContainsKey (args.Entity.DisplayName))
				_contactUidLookup[args.Entity.DisplayName] = args.Entity.UniqueIdentifier;
			else
				_contactUidLookup.Add (args.Entity.DisplayName, args.Entity.UniqueIdentifier);
		}
		
		private void HandleTopicChanged (object sender, EntityChangeEventArgs<string> args)
		{
			IrcChannel channel = args.Entity as IrcChannel;
			
			LogEvent (String.Format ("{0}'s topic has been changed to:\n{0}", channel.DisplayName, args.New));
			
			if (ChannelTopicChanged != null)
				ChannelTopicChanged (this, args);
		}
		
		void HandleChannelModeChanged(object sender, ChannelModeEventArgs e)
		{
			LogEvent (String.Format ("{0} changed channel modes: {1}{2}", e.Contact.DisplayName, e.Value ? "+":"-", IrcProtocolHelper.GetChannelModeString (e.ChannelModeFlag)));
			
			if (ChannelModeChanged != null)
				ChannelModeChanged (this, e);
		}
		
		public void EmitMessageReceived (IrcContact contact, string message)
		{
			IEnumerable<IMessageChunk> chunks = IrcProtocolHelper.ParseMessage (message);
			IMessage msg = new Message (MessageFlag.Message, contact, _session.Account);
			msg.Chunks.AddRange (chunks);
			
			MessageEventArgs args = new MessageEventArgs (msg);
			
			ActivityUtility.EmitActivity (this, new ReceivedMessageActivity (this, msg));
			
			PrimaryContact.EmitPresenceChange (new EntityChangeEventArgs<IPresence>(PrimaryContact, PrimaryContact.Presence, PrimaryContact.Presence));
			
			OnMessageReceived (args);
			
			LogMessage (msg, Ready);
		}
		
		public void EmitChannelBanned ()
		{
			if (ChannelBanned != null)
				ChannelBanned (this, new ObjectEventArgs(null));
		}
		
		public void EmitChannelInviteOnly ()
		{
			if (ChannelInviteOnly != null)
				ChannelInviteOnly (this, new ObjectEventArgs (null));
		}
		
		public void EmitNoticeReceived (IrcContact contact, string message)
		{
			if (NoticeReceived != null)
			{
				IEnumerable<IMessageChunk> chunks = IrcProtocolHelper.ParseMessage (message);
				IMessage msg = new Message (MessageFlag.Event, contact, Session.Account);
				msg.Chunks.AddRange (chunks);
				
				LogMessage (msg, Ready);
				
				NoticeReceived (this, new MessageEventArgs (msg));
			}
		}
		
		public void EmitChannelContactInitialJoin (ChannelContactEventArgs args)
		{
			if (ChannelContactInitialJoin != null)
				ChannelContactInitialJoin (this, args);
		}
		
		public void EmitChannelContactJoin (ChannelContactEventArgs args)
		{
			LogEvent (String.Format ("{0} joined {1}", args.Contact.DisplayName, args.Channel.DisplayName));
			
			if (ChannelContactJoin != null)
				ChannelContactJoin (this, args);
		}
		
		public void EmitChannelContactPart (ChannelContactEventArgs args)
		{
			LogEvent (String.Format ("{0} left {1}", args.Contact.DisplayName, args.Channel.DisplayName));
			
			if (ChannelContactPart != null)
				ChannelContactPart (this, args);
		}
		
		public void EmitChannelContactQuit (ChannelContactMessageEventArgs args)
		{
			LogEvent (String.Format ("{0} quit {1} ({2})", args.Contact.DisplayName, args.Channel.DisplayName, args.Message));
			
			if (ChannelContactQuit != null)
				ChannelContactQuit (this, args);
		}
		
		public void EmitChannelContactKill (ChannelContactActionEventArgs args)
		{
			LogEvent (String.Format ("{0} was killed by {1} ({2})", args.Contact.DisplayName, args.Initiator, args.Message));
			
			if (ChannelContactKill != null)
				ChannelContactKill (this, args);
		}
		
		public void EmitChannelContactKick (ChannelContactActionEventArgs args)
		{
			LogEvent (String.Format ("{0} was kicked from {1} by {2} ({3})", args.Contact.DisplayName, args.Channel.DisplayName, args.Initiator, args.Message));
			
			if (ChannelContactKick != null)
				ChannelContactKick (this, args);
		}
		
		public void EmitChannelContactModeChanged (ChannelContactModeEventArgs args)
		{
			LogEvent (String.Format ("{0} changed channel modes: {1} {2}", args.Actor.DisplayName, args.Message, args.Contact.DisplayName));
			
			if (ChannelContactModeChanged != null)
				ChannelContactModeChanged (this, args);
		}
	}
}